﻿using System;
using NthDownload.Contracts;
using System.IO;
using System.Threading;
using System.ComponentModel;
using NthDownload.Agent.EventArguments;
using NthDownload.Contracts.Plugins.Downloads;

namespace NthDownload.Agent
{
    public class DownloadThread : INotifyPropertyChanged
    {
        #region Events

        #region Started

        /// <summary>
        /// Occurs when the thread starts downloading
        /// </summary>
        public event EventHandler Started;

        /// <summary>
        /// Invokes the Started event
        /// </summary>
        protected void OnStarted()
        {
            if (Started != null)
            {
                Started.Invoke(this, new EventArgs());
            }
        }

        #endregion Started

        #region Stopped

        /// <summary>
        /// Occurs when the thread is finishes, canceled or aborted
        /// </summary>
        public event EventHandler<DownloadThreadStoppedEventArgs> Stopped;

        /// <summary>
        /// Invokes the Stopped event
        /// </summary>
        protected void OnStopped(Exception errorException, bool aborted, bool canceled, bool bandwidthDepleted)
        {
            if (Stopped != null)
            {
                Stopped.Invoke(this, new DownloadThreadStoppedEventArgs(errorException, aborted, canceled, bandwidthDepleted));
            }
        }

        #endregion Stopped

        #region ChunkProcessed

        /// <summary>
        /// Occurs when a chunk has been successfully processed
        /// </summary>
        public event EventHandler<DownloadThreadChunkProcessedEventArgs> ChunkProcessed;

        /// <summary>
        /// Invokes the Stopped event
        /// </summary>
        protected void OnChunkProcessed(long chunkSize)
        {
            if (ChunkProcessed != null)
            {
                ChunkProcessed.Invoke(this, new DownloadThreadChunkProcessedEventArgs(chunkSize));
            }
        }

        #endregion ChunkProcessed

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion INotifyPropertyChanged

        #endregion Events

        #region Class Members

        private bool _cancel;
        private bool _isRunning;

        #endregion Class Members

        #region Constructor

        /// <summary>
        /// Downloads a segment of data on an independant thread
        /// </summary>
        /// <param name="downloadProcessor">The download processor thread belongs to</param>
        /// <param name="segment">The segment to download</param>
        /// <param name="download">The download the segment belongs to</param>
        public DownloadThread(DownloadProcessor downloadProcessor, DownloadSegment segment, IDownload download)
        {
            Segment = segment;
            Download = download;
            DownloadProcessor = downloadProcessor;
            GetSynchronizationContext();

            //
            // Instantiate the worker thread
            //
            WorkerThread = new Thread(DoDownload);
        }

        #endregion Constructor

        #region Private Properties

        /// <summary>
        /// The download processor which owns this thread
        /// </summary>
        private DownloadProcessor DownloadProcessor { get; set; }

        /// <summary>
        /// The segment of the stream to download
        /// </summary>
        private IDownload Download { get; set; }

        /// <summary>
        /// The segment of the stream to download
        /// </summary>
        private DownloadSegment Segment { get; set; }

        /// <summary>
        /// The Synchronization Context of teh executing thread
        /// </summary>
        private SynchronizationContext CurrentSynchronizationContext { get; set; }

        /// <summary>
        /// The thread all the downloading work is done on
        /// </summary>
        private Thread WorkerThread { get; set; }

        #endregion Private Properties

        #region Properties

        /// <summary>
        /// Indicates if the thread is running
        /// </summary>
        public bool IsRunning
        {
            get
            {
                return _isRunning;
            }
            private set
            {
                _isRunning = value;
                OnPropertyChanged("IsRunning");
            }
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Starts the thread
        /// </summary>
        public void Start()
        {
            if (IsRunning) return;

            IsRunning = true;
            OnStarted();

            WorkerThread.Start();
        }

        /// <summary>
        /// Signal the thread to stop. This will stop the download safely but
        /// will wait for the current buffer finish downloading and written.
        /// </summary>
        public void Stop()
        {
            _cancel = true;
        }

        /// <summary>
        /// Stop the thread. This will stop the immediately with out finishing
        /// the current buffer.
        /// </summary>
        public void Abort()
        {
            WorkerThread.Abort();
        }

        #endregion Methods

        #region Private Methods

        /// <summary>
        /// Opens a new stream form the download, reads the data for the 
        /// segment this thread is processing and writes it to the temp file
        /// </summary>
        private void DoDownload()
        {
            IDownloadStream downloadStream = null;
            FileStream tempFileStream = null;
            Exception errorException = null;
            bool aborted = false;
            bool canceled = false;
            bool bandwidthDepleted = false;

            try
            {
                //
                // Open download & temp file streams
                //
                downloadStream = Download.OpenStream(Segment.CurrentPosition, Segment.EndPosition);
                tempFileStream = new FileStream(Download.DestinationPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write, Download.ChunkSize);

                //
                // Read the segment from the download stream & write it to 
                // the temp file.
                //
                byte[] buffer = new byte[DownloadProcessor.ChunkSize];
                int bytesRead = 0;
                bool isBandwidthAvailable = true;
                bool isEndOfInifinte = false;

                //downloadStream.Seek(Segment.CurrentPosition, SeekOrigin.Begin);
                tempFileStream.Seek(Segment.CurrentPosition, SeekOrigin.Begin);

                while (!Segment.IsFinished && !isEndOfInifinte && (bytesRead > 0 || (bytesRead == 0 && isBandwidthAvailable)))
                {
                    //
                    // Get chunk size
                    //
                    int desiredChunkSize = (Segment.IsInfinite) ? DownloadProcessor.ChunkSize : Math.Min(DownloadProcessor.ChunkSize, (int)Segment.SizeRemaining);
                    int actualChunkSize = (DownloadProcessor.BandwidthProvider != null) ? DownloadProcessor.BandwidthProvider.Take(desiredChunkSize) : desiredChunkSize;
                    isBandwidthAvailable = (DownloadProcessor.BandwidthProvider != null) ? DownloadProcessor.BandwidthProvider.HasBandwidth : true;

                    if (actualChunkSize > 0)
                    {
                        //
                        // Read from the stream
                        //
                        bytesRead = downloadStream.Stream.Read(buffer, 0, actualChunkSize);

                        //
                        // Write to temp file
                        //
                        tempFileStream.Write(buffer, 0, bytesRead);

                        //
                        // Detect end of infinite segment
                        //
                        isEndOfInifinte = (Segment.IsInfinite) ? bytesRead == 0 : false;

                        //
                        // Update segment
                        //
                        int read = bytesRead;
                        CurrentSynchronizationContext.Send(delegate
                        {
                            Segment.CurrentPosition += read;
                            OnChunkProcessed(read);
                        }, null);
                    }
                    else
                    {
                        //
                        // Set that no bytes where read
                        //
                        bytesRead = 0;

                        //
                        // Check if bandwidth has been depleted
                        //
                        bandwidthDepleted = !isBandwidthAvailable;
                    }

                    //
                    // Check for cancel
                    //
                    if (_cancel)
                    {
                        canceled = true;
                        break;
                    }
                }

                //
                // Set infinite segments end position if the download completed with out cancelation
                //
                if (isEndOfInifinte)
                {
                    CurrentSynchronizationContext.Send(delegate 
                    {
                        Segment.EndPosition += Segment.CurrentPosition;
                    }, null);
                }
            }
            catch (ThreadAbortException)
            {
                aborted = true;
            }
            catch (Exception e)
            {
                errorException = e;
            }
            finally
            {
                //
                // Close streams, fail silently if there is an issue, when logging
                // is introduced log and event instead of failing siltenly.
                //
                try
                {
                    if (tempFileStream != null)
                    {
                        tempFileStream.Close();
                    }
                }
                catch { }

                try
                {
                    if (downloadStream != null)
                    {
                        downloadStream.Close();
                    }
                }
                catch { }

                //
                // Set status update and raise stopped event on the calling thread.
                //
                CurrentSynchronizationContext.Send(delegate
                {
                    IsRunning = false;
                    OnStopped(errorException, aborted, canceled, bandwidthDepleted);
                }, null);
            }
        }

        /// <summary>
        /// Get the synchronization context of the calling thread.
        /// </summary>
        private void GetSynchronizationContext()
        {
            CurrentSynchronizationContext = SynchronizationContext.Current;

            if (CurrentSynchronizationContext == null)
            {
                throw new Exception("Executing thread has no SynchronizationContext.");
            }
        }

        #endregion Private Methods
    }
}
